home *** CD-ROM | disk | FTP | other *** search
/ Nebula 2 / Nebula Two.iso / SourceCode / GameKit / gamekit-1 / ScorePlayer.m < prev    next >
Text File  |  1995-06-12  |  15KB  |  478 lines

  1. // The ScorePlayer object handles reading scorefiles as
  2. // well as starting and stopping playback.  Playback is
  3. // done in a separate thread.  Errors are ignored.
  4.  
  5. // This code is a highly modified version of the code
  6. // used in the ScorePlayer.app music kit example.
  7. // I have added several methods and removed most of the
  8. // error messages, since the errors are not useful in a game.
  9.  
  10. // Note: if you plan to use sounds simultaneously with the
  11. // music, you CANNOT use the NeXT Sound object's -play
  12. // method!!!  You have to allocate and set up your own
  13. // SoundOut and PlayStream objects and go through them.
  14. // You can use the Sound object to store/convert the data,
  15. // but not play it.  See the SoundPlayer.[hm] files in this
  16. // distribution to see how to accomplish this.
  17.  
  18. // This should be the default score to be loaded:
  19. #define defaultFileName "Default.score"
  20.  
  21. #import <gamekit/gamekit.h>
  22. #import <daymisckit/daymisckit.h>
  23. #import <objc/NXBundle.h>
  24. #import <musickit/musickit.h>
  25. #import <string.h>
  26. #import <libc.h>
  27. #import <mach/cthreads.h>
  28. #import <mach/mach.h>
  29. #import <mach/mach_error.h>
  30. #import    <mach/message.h>
  31. #import <objc/objc-runtime.h>
  32.  
  33. @implementation ScorePlayer
  34.  
  35. // Strings used in alert panel.  Ought to be localized eventually.
  36. #define OBJECTNAME "Load Score"
  37. #define CANTLOAD "Unable to load music score file."
  38. #define OK "OK"
  39.  
  40. static BOOL playScoreForm;
  41. static id synthInstruments;
  42. static id openPanel;
  43. static char* fileName;
  44. static id scoreObj,scorePerformer,theOrch;
  45. static double samplingRate = 22050;
  46. static double headroom = .1;
  47. static BOOL userCancelFileRead = NO;
  48. static double initialTempo = 60.0;
  49. static double lastTempo = 60.0;
  50. static double desiredTempo = 60.0;
  51. static char *fileSuffixes[3] = {"score","playscore",NULL};
  52. static id condClass = nil;
  53. static id midis[2] = {0};
  54. static int midiOffset;
  55. static BOOL errorDuringPlayback = NO;
  56. static BOOL firstPlay = YES;
  57.  
  58. #define PLAYING ([condClass performanceThread] != NO_CTHREAD)
  59.  
  60. #define SOUND_OUT_PAUSE_BUG 1    // Workaround for problem synching MIDI to DSP
  61.  
  62. static int handleObjcError(const char *className)
  63. { // ignore objc errors (like missing synthpatch classes)
  64.     return 0;
  65. }
  66.  
  67. static void handleMKError(char *msg)
  68. { // ignore all errors
  69.     if (!PLAYING) {    // if can't read file (ie. parse error), cancel read
  70.         userCancelFileRead = YES;
  71.     }
  72. }
  73.  
  74. void cantLoad()
  75. {
  76.     NXRunAlertPanel(OBJECTNAME, CANTLOAD, OK, NULL, NULL);
  77. }
  78.  
  79. - _loadFile
  80. { // actually loads in the scorefile
  81.     id tuningSys;
  82.     id scoreInfo; 
  83.     haveScore = NO; firstPlay = YES;                                  
  84.     MKSetScorefileParseErrorAbort(10);
  85.     if ((!fileName) || (!strlen(fileName))) { /* Can this ever happen? */ 
  86.         return nil;
  87.     }
  88.     playScoreForm = (strstr(fileName,".playscore") != NULL);
  89.     [scoreObj free];
  90.     scoreObj = [Score new];
  91.     userCancelFileRead = NO;
  92.     tuningSys = [[TuningSystem alloc] init]; /* 12-tone equal tempered */
  93.     [tuningSys install];
  94.     [tuningSys free];
  95.     if (![scoreObj readScorefile:(char *)fileName] || userCancelFileRead) {  
  96.         cantLoad();
  97.         scoreObj = [scoreObj free];
  98.         fileName[0] = '\0';
  99.         return nil;
  100.     }
  101.     samplingRate = 22050;
  102.     headroom = .1;
  103.     initialTempo = 60.0;
  104.     [[condClass defaultConductor] setTempo:initialTempo];
  105.     scoreInfo = [(Score *)scoreObj info];
  106.     if (scoreInfo) { /* Configure performance as specified in info. */ 
  107.     int midiOffsetPar;
  108.     midiOffset = 0;
  109.     midiOffsetPar = [Note parName:"midiOffset"];
  110.     if ([scoreInfo isParPresent:midiOffsetPar])
  111.         midiOffset = [scoreInfo parAsDouble:midiOffsetPar];
  112.     if ([scoreInfo isParPresent:MK_headroom])
  113.         headroom = [scoreInfo parAsDouble:MK_headroom];      
  114.     if ([scoreInfo isParPresent:MK_samplingRate]) {
  115.         samplingRate = [scoreInfo parAsDouble:MK_samplingRate];
  116.         if (!((samplingRate == 44100.0) || (samplingRate == 22050.0))) {
  117.             samplingRate = 22050; // has to be one or the other!
  118.         }
  119.     }
  120.     if ([scoreInfo isParPresent:MK_tempo]) {
  121.         initialTempo = [scoreInfo parAsDouble:MK_tempo];
  122.         [[condClass defaultConductor] setTempo:initialTempo];
  123.     } 
  124.         #if SOUND_OUT_PAUSE_BUG
  125.     if (samplingRate == 22050)
  126.         midiOffset +=  .36363636363636/8.0;
  127.     else midiOffset += .181818181818181/8.0;
  128.         #else
  129.     if (samplingRate == 22050)
  130.         midiOffset +=  .36363636363636;
  131.     else midiOffset += .181818181818181;
  132.         #endif
  133.     /* Note: there is a .1 second indeterminacy (in the 22khz case) due 
  134.        to not knowing where we are in soundout buffering. Using more, 
  135.        but smaller buffers would solve this. */
  136.     } 
  137.     lastTempo = desiredTempo = initialTempo;
  138.     haveScore = YES;
  139.     return self;
  140. }
  141.  
  142. static port_t endOfTimePort = PORT_NULL;
  143.  
  144. -endOfTime    // called by the musickit thread
  145. {    // when a performance completes
  146. //    int i;
  147.     msg_header_t msg =    {0,                   /* msg_unused */
  148.                            TRUE,                /* msg_simple */
  149.                sizeof(msg_header_t),/* msg_size */
  150.                MSG_TYPE_NORMAL,     /* msg_type */
  151.                0};                  /* Fills in remaining fields */
  152.     [theOrch close]; /* This will block! */
  153. //    for (i=0; i<2; i++) {
  154. //    [midis[i] close];
  155. //    midis[i] = nil;
  156. //    }
  157.     [theOrch setSoundOut:YES];
  158.     msg.msg_local_port = PORT_NULL;
  159.     msg.msg_remote_port = endOfTimePort;
  160.     msg_send(&msg, SEND_TIMEOUT, 0);
  161.     return self;
  162. }
  163.  
  164. void *endOfTimeProc(msg_header_t *msg,ScorePlayer *myself )
  165. {
  166.     // Tell delegate that the score finished.
  167.     [myself scoreFinishedPlaying];
  168.     return myself;
  169. }
  170.  
  171. static BOOL isMidiClassName(char *className)
  172. {
  173.     return (className && ((strcmp(className,"midi") == 0)  ||
  174.               (strcmp(className,"midi1") == 0) ||
  175.               (strcmp(className,"midi0") == 0)));
  176. }
  177.  
  178. #if SOUND_OUT_PAUSE_BUG
  179.  
  180. static BOOL checkForMidi(Score *obj)
  181. {
  182.     id subobjs;
  183.     int i,cnt;
  184.     id info;
  185.     subobjs = [obj parts];
  186.     if (!subobjs)
  187.       return NO;
  188.     cnt = [subobjs count];
  189.     for (i=0; i<cnt; i++) {
  190.     info = [(Part *)[subobjs objectAt:i] info];
  191.     if ([info isParPresent:MK_synthPatch] &&
  192.         (isMidiClassName([info parAsStringNoCopy:MK_synthPatch]))) {
  193.         [subobjs free];
  194.         return YES;
  195.     }
  196.     }
  197.     [subobjs free];
  198.     return NO;
  199. }
  200. #endif
  201.  
  202. - _playIt
  203. { // initiate playback in separate MK thread
  204.     int partCount,synthPatchCount,voices,i,whichMidi,midiChan;
  205.     char *className;
  206.     id partPerformers,synthPatchClass,partPerformer,partInfo,anIns,aPart;
  207.  
  208. // if (firstPlay) {   /* Could keep these around, in repeat-play cases: */ 
  209. //    scorePerformer = [scorePerformer free];
  210. //    [synthInstruments freeObjects];
  211. //    synthInstruments = [synthInstruments free];
  212. //}
  213.     theOrch = [Orchestra newOnDSP:0]; /* A noop if it exists */
  214.     [theOrch setHeadroom:headroom];    /* Must be reset for each play */ 
  215.     [theOrch setSamplingRate:samplingRate];
  216. #if SOUND_OUT_PAUSE_BUG
  217.     if (checkForMidi(scoreObj))
  218.     [theOrch setFastResponse:YES];
  219.     else [theOrch setFastResponse:NO];
  220. #endif
  221.     [theOrch setOutputCommandsFile:NULL];
  222.     [theOrch setOutputSoundfile:NULL];
  223.     [theOrch setSoundOut:YES];
  224.     if (![theOrch open]) {    // can't get DSP, so abort
  225.         return nil;
  226.     }
  227. //if (firstPlay) {
  228.     scorePerformer = [ScorePerformer new];
  229.     [scorePerformer setScore:scoreObj];
  230.     [(ScorePerformer *)scorePerformer activate]; 
  231.     partPerformers = [scorePerformer partPerformers];
  232.     partCount = [partPerformers count];
  233.     synthInstruments = [List new];
  234.     for (i = 0; i < partCount; i++) {
  235.     partPerformer = [partPerformers objectAt:i];
  236.     aPart = [partPerformer part]; 
  237.     partInfo = [(Part *)aPart info];      
  238.     if ((!partInfo) || ![partInfo isParPresent:MK_synthPatch]) {
  239.         continue; // missing parm.  Just ignore.
  240.     }        
  241.     className = [partInfo parAsStringNoCopy:MK_synthPatch];
  242.     if (isMidiClassName(className)) {
  243.         midiChan = [partInfo parAsInt:MK_midiChan];
  244.         if ((midiChan == MAXINT) || (midiChan > 16))
  245.         midiChan = 1;
  246.         if (strcmp(className,"midi") == 0)
  247.         className = "midi1";
  248.         if (strcmp(className,"midi1") == 0) 
  249.         whichMidi = 1;
  250.         else whichMidi = 0;
  251.         if (midis[whichMidi] == nil)
  252.         midis[whichMidi] = [Midi newOnDevice:className];
  253.         [[partPerformer noteSender] connect:
  254.          [midis[whichMidi] channelNoteReceiver:midiChan]];
  255.     } else {
  256.         synthPatchClass = (strlen(className) ? 
  257.                    [SynthPatch findSynthPatchClass:className] : nil);
  258.         if (!synthPatchClass) {         /* Class not loaded in program? */
  259.             haveScore = NO;
  260.             cantLoad();
  261.             return nil;
  262.         /* We would prefer to do dynamic loading here. */
  263.         }
  264.         anIns = [SynthInstrument new];      
  265.         [synthInstruments addObject:anIns];
  266.         [[partPerformer noteSender] connect:[anIns noteReceiver]];
  267.         [anIns setSynthPatchClass:synthPatchClass];
  268.         if (![partInfo isParPresent:MK_synthPatchCount])
  269.         continue;         
  270.         voices = [partInfo parAsInt:MK_synthPatchCount];
  271.         synthPatchCount = 
  272.         [anIns setSynthPatchCount:voices patchTemplate:
  273.          [synthPatchClass patchTemplateFor:partInfo]];
  274.         if (synthPatchCount < voices) { // ignore problem
  275.         }
  276.     }
  277.     }
  278. //    [partPerformers free];
  279. //}
  280.     errorDuringPlayback = NO;
  281.     MKSetDeltaT(.75);
  282.     [Orchestra setTimed:YES];
  283.     [condClass afterPerformanceSel:@selector(endOfTime) to:self argCount:0];
  284.     for (i=0; i<2; i++) 
  285.         [midis[i] openOutputOnly]; /* midis[i] is nil if not in use */
  286.     for (i=0; i<2; i++) 
  287.         if (midiOffset > 0) 
  288.             [midis[i] setLocalDeltaT:midiOffset];
  289.         else if (midiOffset < 0)
  290.             [theOrch setLocalDeltaT:-midiOffset];
  291.     for (i=0; i<2; i++) 
  292.     [midis[i] run]; firstPlay = NO;
  293.     [theOrch run];
  294.     [condClass startPerformance];     
  295.     return self;
  296. }
  297.  
  298. extern void _MKSetConductorThreadMaxStress(int arg);
  299.  
  300. - init
  301. { // set up our object.  I really ought to change to using a +new
  302.   // type of method since there should only ever be one ScorePlayer.
  303.     static int inited = 0;
  304.     int ec;
  305.     [super init];
  306.     if (inited++)
  307.       return self;
  308.     haveScore = NO;
  309.     condClass = [Conductor class];
  310.     [condClass setThreadPriority:1.0];
  311.     setuid(getuid()); /* Must be after setThreadPriority. */
  312.     [condClass useSeparateThread:YES];
  313.     /* These numbers could be endlessly tweaked */
  314.     MKSetLowDeltaTThreshold(.25);
  315.     MKSetHighDeltaTThreshold(.4);
  316.     _MKSetConductorThreadMaxStress(1000000); /* Don't do cthread_yields */
  317.     ec = port_allocate(task_self(), &endOfTimePort);
  318.     DPSAddPort(endOfTimePort,(DPSPortProc)endOfTimeProc,
  319.            sizeof(msg_header_t),(void *)self,30);
  320.     MKSetErrorProc(handleMKError);
  321.     objc_setClassHandler(handleObjcError);
  322.     return self;
  323. }
  324.  
  325. - appDidInit:sender    // forwarded by GameBrain -- just loads score
  326. {
  327.     [self loadFile];
  328.     return self;
  329. }
  330.  
  331. int setUpFile()
  332. { // use open panel to grab a score/playscore file.
  333.     int success;
  334.     char *shortFileName, *dir;
  335.     static BOOL firstTime = YES;
  336.     if (!openPanel)
  337.         openPanel = [OpenPanel new];    
  338.     if ((firstTime) && !fileName)
  339.       success = [openPanel 
  340.            runModalForDirectory:"/LocalLibrary/Music/Scores"
  341.            file:"Examp1.score" 
  342.            types:(const char *const *)fileSuffixes]; 
  343.     else if (fileName) { // split into dir & name & run open panel
  344.         dir = NXCopyStringBuffer((const char *)fileName);
  345.         shortFileName = rindex(dir, '/') + 1;
  346.         shortFileName[0] = '\0'; // isolate directory
  347.         shortFileName = rindex(fileName, '/') + 1; // isolate filename
  348.         success = [openPanel 
  349.              runModalForDirectory:dir
  350.              file:shortFileName 
  351.              types:(const char *const *)fileSuffixes]; 
  352.         free(dir);
  353.     } else success = [openPanel 
  354.             runModalForTypes:(const char *const *)fileSuffixes];
  355.     if (!success) return NO;
  356.     fileName = NXCopyStringBuffer((const char *)[openPanel filename]);
  357.     // save the choice.
  358.     NXWriteDefault ([NXApp appName], "ScoreName", fileName);
  359.     firstTime = NO;
  360.     return YES;
  361. }
  362.  
  363. - _abort
  364. { // abort (stop) a performance
  365.     int i;
  366.     if (PLAYING) {
  367.     [condClass lockPerformance];
  368.     for (i=0; i<2; i++) 
  369.       if (midis[i]) {
  370.           [midis[i] allNotesOff];
  371.           [midis[i] abort];
  372.       }
  373.     [theOrch abort];
  374.     [condClass finishPerformance];
  375.     [condClass unlockPerformance];
  376.     cthread_yield();
  377.     while (PLAYING) ; /* Make sure it's really done. */
  378.     }
  379.     return self;
  380. }
  381.  
  382.  
  383. // loading a file always stops playback, but restarts playing after
  384. // the new file is loaded if music was playing before the load.
  385. // this is the most useful behavior for a game, IMHO...
  386. // to change this, make a subclass that does something like this
  387. // for all three score file loading methods:
  388. //
  389. // -loadfile { [self stop:self]; return [super loadFile]; }
  390.  
  391. - loadFile
  392. { // load default file in.
  393.     BOOL wasPlaying = PLAYING; char *slashPos;
  394.     const char *tmpstr = NXGetDefaultValue ([NXApp appName], "ScoreName");
  395.     aborted = YES;
  396.     if (PLAYING) [self _abort];
  397.     if (fileName) free(fileName);
  398.     if (!tmpstr) { // if no default yet, use built in score
  399.         fileName = malloc(MAXPATHLEN);
  400.         strcpy(fileName, NXArgv[0]);
  401.         if (slashPos = strrchr(fileName, '/')) {
  402.             slashPos[1] = '\0';
  403.         } else {
  404.             strcpy(fileName, "./");
  405.         }
  406.         strcat(fileName, defaultFileName);
  407.     } else fileName = NXCopyStringBuffer(tmpstr);
  408.     [self _loadFile];
  409.     if (wasPlaying) [self play:self];
  410.     return self;
  411. }
  412.  
  413. - readScoreFile:(const char *)pathName;    // open scorefile (full pathname)
  414. { // get a scorefile.  give full path!
  415.     BOOL wasPlaying = PLAYING;
  416.     aborted = YES;
  417.     if (PLAYING) [self _abort];
  418.     strcpy(fileName, pathName);
  419.     [self _loadFile];
  420.     if (wasPlaying) [self play:self];
  421.     return self;
  422. }
  423.  
  424. - selectFile:sender
  425. { // get the scorefile to use
  426.     BOOL wasPlaying = PLAYING;
  427.     aborted = YES;
  428.     if (PLAYING) [self _abort];
  429.     if (!setUpFile(NULL)) {
  430.       return self;
  431.     }
  432.     [self _loadFile];
  433.     if (wasPlaying) [self play:self];
  434.     return self;
  435. }
  436.  
  437. - play:sender
  438. { // initiate a performance
  439.     if ((!haveScore) || (!fileName) || (!strlen(fileName))) return nil;
  440.     if (PLAYING) return self;
  441.     aborted = NO;
  442.     [self _playIt];
  443.     return self;
  444. }
  445.  
  446. - stop:sender
  447. { // stop a performance
  448.     aborted = YES;
  449.     if (PLAYING) [self _abort];
  450.     return self;
  451. }
  452.  
  453. // set up a delegate
  454. - delegate { return delegate; }
  455. - setDelegate:newDelegate
  456. {
  457.     id oldDelegate = delegate;
  458.     delegate = newDelegate;
  459.     return oldDelegate;
  460. }
  461.  
  462. // delegate can implement this to be notified when a score
  463. // finishes playing.  If no delegate, default implementation
  464. // is to start playing the score again.
  465. - scoreFinishedPlaying
  466. {
  467.     if (delegate) {
  468.         if ([delegate respondsTo:@selector(scoreFinishedPlaying)])
  469.             return [delegate scoreFinishedPlaying];
  470.     } else {    // restart unless we were sent a -stop: message
  471.         if (!aborted) return [self play:self];
  472.     }
  473.     return self; // never actually get here but suppresses a warning
  474. }
  475.  
  476. @end
  477.  
  478.